package edu.upol.paradise.hypervisor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;

import org.apache.log4j.Logger;

import edu.upol.paradise.hypervisor.protocol.PingPacket;
import edu.upol.paradise.hypervisor.protocol.PongPacket;
import edu.upol.paradise.hypervisor.protocol.RequestCNLPacket;
import edu.upol.paradise.hypervisor.protocol.SendCNLPacket;
import edu.upol.paradise.hypervisor.protocol.ServicePacket;
import edu.upol.paradise.hypervisor.protocol.TCPConnector;
import edu.upol.paradise.utils.HypervisorUtils;

public class NodeClusterList {
	static final Logger log = Logger.getLogger(NodeClusterList.class);
	private static final Random random = new Random();
	private static final int    MIN_NODES_REQUIRED = 128;
	
	public static class NodeCluster {
		private boolean valid = true;
		public String addr;
		public int port;

		public boolean isValid() {
			return valid;
		}

		public void setInvalid() {
			valid = false;
		}
		
		public NodeCluster(String addr, int port){
			this.addr = addr;
			this.port = port;
		}

		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + ((addr == null) ? 0 : addr.hashCode());
			result = prime * result + port;
			return result;
		}

		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			NodeCluster other = (NodeCluster) obj;
			if (addr == null) {
				if (other.addr != null)
					return false;
			} else if (!addr.equals(other.addr))
				return false;
			if (port != other.port)
				return false;
			return true;
		}
		
		@Override
		public String toString(){
			return (valid ? "VALID " : "INVALID ") + "Node Cluster <" + addr + ":" + port + ">";
		}
	}
	
	private Set<NodeCluster> clusterSet = new HashSet<NodeCluster>();
	private List<NodeCluster> clusterList = new ArrayList<NodeCluster>();
	
	public NodeClusterList(){
		
	}
	
	public synchronized void addClusterNode(String addr, int port){
		NodeCluster cluster = new NodeCluster(addr, port);
		clusterSet.add(cluster);
		clusterList.add(cluster);
	}
	
	public synchronized void checkNodes(){
		for (NodeCluster cluster : new HashSet<NodeCluster>(clusterSet)) {
			if (invalid(cluster)){
				clusterSet.remove(cluster);
				clusterList.remove(cluster);
			}
		}
	}
	
	private boolean invalid(NodeCluster cluster) {
		if (!cluster.isValid())
			return true;
		
		return checkCluster(cluster);
	}

	private boolean checkCluster(NodeCluster cluster) {
		TCPConnector connector = null;
		try {
			connector = new TCPConnector(cluster.addr, cluster.port, 3000);
			connector.send(new PingPacket());
			ServicePacket sp = ServicePacket.asServicePacket(connector.accept());
			if (sp == null)
				return true;
			if (!(sp instanceof PongPacket))
				return true;
			return false;
		} catch (Exception e) {
			log.error(e);
			log.debug(e, e);
			return true;
		} finally {
			try {
				if (connector != null)
					connector.terminate();
			} catch (Exception e) {
				log.error(e);
				log.debug(e, e);
			}
		}
	}

	public synchronized NodeCluster get(){
		while (true){
			NodeCluster cluster = clusterList.get(random.nextInt(clusterList.size()));
			if (cluster.isValid())
				return cluster;
		}
	}

	public void acquireNodesFromNodes() {
		int itCount = 0;
		while (true){
			++itCount;
			if (itCount > 1000){
				log.info("Failed to build at least " + MIN_NODES_REQUIRED + " node clusters");
				break;
			}
			
			for (NodeCluster c : Collections.unmodifiableList(clusterList)){
				if (c.valid && !HypervisorUtils.isCurrentHypervisor(c.addr, c.port)){
					requestNodeClusterList(c);
				}
			}
		}
	}

	private void requestNodeClusterList(NodeCluster c) {
		try {
			TCPConnector connector = new TCPConnector(c.addr, c.port, 3000);
			connector.send(new RequestCNLPacket());
			ServicePacket sp = ServicePacket.asServicePacket(connector.accept());
			if (sp == null)
				return;
			if (!(sp instanceof SendCNLPacket)){
				for (NodeCluster cls : ((SendCNLPacket)sp).fetchClusters())
					addClusterNode(cls.addr, cls.port);
			}
				
		} catch (Exception e){
			c.setInvalid();
		}
	}

	public synchronized List<NodeCluster> getAllAsList() {
		return Collections.unmodifiableList(clusterList);
	}
}
